/*******************************************************************************
 * @file Copyright 2009 The CREATE Lab at CMU
 * 
 *       This file is part of JubJub, an open source project.
 *       http://code.google.com/p/jubjub/
 * 
 *       Redistribution and use in source and binary forms, with or without
 *       modification, are permitted provided that the following conditions are
 *       met:
 * 
 *       Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 * 
 *       Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 * 
 *       Neither the name of Carnegie Mellon nor the names of its contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 * 
 *       THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *       "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *       LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 *       PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 *       HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *       SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 *       LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *       DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 *       THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 *       (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 *       OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

package edu.cmu.ri.createlab.jj.projects;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import edu.cmu.ri.createlab.jj.JubJub.EmptyStringException;
import edu.cmu.ri.createlab.jj.blocks.*;
import edu.cmu.ri.createlab.jj.blocks.BlockDefs.BlockTypes;
import edu.cmu.ri.createlab.jj.blocks.BlockDefs.CallTypes;
import edu.cmu.ri.createlab.jj.visitors.*;

/**
 * 
 * @author Erik Pasternak
 *
 * This is the JubJub representation of a class. It contains a list of
 * all the variables (including objects) defined at the class scope, a
 * list of constructors for the class, and a list of inline and method
 * blocks. 
 * 
 * While the JubJub editor treats inline and method blocks in the same
 * way when outputting code the methodBlocks are visited with references
 * to inlineBlocks copied in directly instead of being called. When
 * outputting to XML there is also no difference between inline and method
 * blocks.
 * 
 */
public class JJClass
{
   /**
    * The name of the class.
    */
   private String name;
   /**
    * The Java friendly call name for this class.
    */
   private String callName;
   /**
    * A list of all the variables defined at the class level for this class.
    * Public Static variables will be accessible directly while non-static
    * public variables will only be accessible through an object. This should
    * be decided and handled by the IDE. The VarBlock part of an Object should
    * be included here.
    */
   private List<VarBlock> vars;
   /**
    * All blocks that are constructors get added to this list. Constructors don't
    * need to be separate from other blocks, this is just done for convenience
    * to make it easy to put all constructors at the top of a file.
    */
   private List<BlockInterface> constructors;
   /**
    * Blocks that don't get written as subroutines are placed here. Instead,
    * the code in these is copied directly into the referencing block when
    * Java output is written. These still get output to XML when saving a
    * project.
    */
   private List<BlockInterface> inlineBlocks;
   /**
    * All blocks that are not constructors or variables go here.
    */
   private List<BlockInterface> methodBlocks;
   /**
    * A class maintains a list of all objects in the project that reference
    * it.
    */
   private List<ObjectBlock> instances;
   /**
    * A list of all the imports this class needs. Each import should be a string of the full name.
    */
   private List<String> imports;
   ///Whether or not this class is valid
   private boolean valid;
   ///Whether there's an error in this class
   private boolean error;
   ///Top level comment for this class
   private String comment;
   
   public JJClass(String name)
      throws Exception
   {
      this.vars = new LinkedList<VarBlock>();
      this.constructors = new LinkedList<BlockInterface>();
      this.inlineBlocks = new LinkedList<BlockInterface>();
      this.methodBlocks = new LinkedList<BlockInterface>();
      this.instances = new LinkedList<ObjectBlock>();
      this.imports = new LinkedList<String>();
      this.setName(name);
      this.valid = true;
      this.comment = "";
      
   }
   
   ///Allows visitors to visit the class.
   public BVReturn accept(BlockVisitorInterface bv)
   {
      return bv.visit(this);
   }
   
   ///Updates the name for all the constructors
   private void updateConstructors()
      throws Exception
   {
      Iterator<BlockInterface> it = constructors.iterator();
      while(it.hasNext())
      {
         it.next().setName(this.name);
      }
   }
   
   ///Updates all instances of this class
   public void updateInstances()
   {
      Iterator<ObjectBlock> it = instances.iterator();
      while(it.hasNext())
         it.next().update();
   }
   
   ///sets this class to invalid and updates its instances
   public void delete()
   {
      this.valid = false;
      updateInstances();
   }
   
   ///sets the class back to valid
   public void undelete()
   {
      this.valid = true;
      updateInstances();
   }
   
   ///returns whether this class is valid or not
   public boolean isValid()
   {
      return valid;
   }

   ///returns the error state for this class
   public boolean hasError()
   {
      return error;
   }
   
   ///Checks its top level contents for errors and sets its error
   ///flag if it finds one. Clears it otherwise.
   public void checkError()
   {
      Iterator<VarBlock> vIt = this.vars.iterator();
      while(vIt.hasNext())
      {
         if(vIt.next().hasError())
         {
            this.error = true;
            return;
         }
      }
      Iterator<BlockInterface> bIt = this.inlineBlocks.iterator();
      while(bIt.hasNext())
      {
         if(bIt.next().hasError())
         {
            this.error = true;
            return;
         }
      }
      bIt = this.methodBlocks.iterator();
      while(bIt.hasNext())
      {
         if(bIt.next().hasError())
         {
            this.error = true;
            return;
         }
      }
      this.error = false;
      return;
   }
   
   ///Clears all the linked lists in the class to help the garbage collector
   public void destroy()
      throws BlockException
   {
      //TODO 
      this.valid = false;
      Iterator<VarBlock> vIt = this.vars.iterator();
      while(vIt.hasNext())
      {
         vIt.next().destroyBlock();
      }
      this.vars.clear();
      Iterator<BlockInterface> bIt = this.constructors.iterator();
      while(bIt.hasNext())
      {
         bIt.next().destroyBlock();
      }
      this.constructors.clear();
      bIt = this.inlineBlocks.iterator();
      while(bIt.hasNext())
      {
         bIt.next().destroyBlock();
      }
      this.inlineBlocks.clear();
      bIt = this.methodBlocks.iterator();
      while(bIt.hasNext())
      {
         bIt.next().destroyBlock();
      }
      this.methodBlocks.clear();
      Iterator<ObjectBlock> oIt = this.instances.iterator();
      while(oIt.hasNext())
      {
         oIt.next().setJJClass(null);
      }
      this.instances.clear();
   }
   
   ///Add a new instance of this class to the list
   public void addInstance(ObjectBlock instance)
   {
      if(!this.instances.contains(instance))
         this.instances.add(instance);
   }
   
   ///Remove an instance of this class from the list
   public void removeInstance(ObjectBlock instance)
   {
      this.instances.remove(instance);
   }
   
   ///Add an import to this class
   public void addImport(String importName)
   {
      this.imports.add(importName);
   }
   
   ///Remove an import
   public void remImport(String importName)
   {
      this.imports.remove(importName);
   }
   
   ///Get the list of imports
   public List<String> getImports()
   {
      return imports;
   }
   
   ///Sets the name and callname for this class.
   public void setName(String name)
      throws Exception
   {
      ///If the name is of 0 length we've made a mistake and need to throw
      ///an exception.
      if(name.length() == 0) 
      {
         throw new EmptyStringException("Empty string name");
      }
      this.name = name;
      ///The callname should be the same as the name with some changes to make it
      ///Java friendly
      this.callName = name.replaceAll("[^a-z&&[^A-Z]&&[^0-9]]+", "_");
      if(!(this.callName.substring(0, 1).matches("[a-zA-z]")))
      {
         this.callName = "a" + this.callName;
      }
      this.updateConstructors();
      this.updateInstances();
   }

   ///Returns the name of this class
   public String getName()
   {
      return name;
   }
   
   ///Returns the call name of this class
   public String getCallName()
   {
      return callName;
   }
   
   ///returns the comment for this class
   public String getComment()
   {
      return comment;
   }
   
   ///sets the comment for this class
   public void setComment(String comment)
   {
      this.comment = comment;
   }

   ///Adds a variable to the list of variables in this class
   public void addVar(VarBlock var)
   {
      this.vars.add(var);
   }
   
   ///Adds a variable in the specified location
   public void addVar(VarBlock var, int index)
   {
      this.vars.add(index, var);
   }
   
   ///Removes the specified variable from this class
   public void remVar(VarBlock var)
   {
      this.vars.remove(var);
   }
   
   ///Returns the list of variables in this class
   public List<VarBlock> getVars()
   {
      return vars;
   }
   
   ///Adds a constructor to this class
   public void addConstructor(BlockInterface constructor)
   {
      this.constructors.add(constructor);
   }
   
   ///Removes a constructor from this class
   public void remConstructor(BlockInterface constructor)
   {
      this.constructors.remove(constructor);
   }
   
   ///Gets the list of constructors for this class
   public List<BlockInterface> getConstructors()
   {
      return constructors;
   }

   ///Runs a set of checks to ensure a block can be added to the inline list
   private boolean isAddableInlineBlock(BlockInterface block)
   {
    ///Make sure we aren't adding an unsupported block type
      if(block.getType() != BlockTypes.CODE
            && block.getType() != BlockTypes.BLOCK)
      {
         return false;
      }
      ///And make sure it's an inline version of it's a code block
      if(block.getType() == BlockTypes.CODE)
      {
         CodeBlock tempBlock = (CodeBlock)block;
         if(tempBlock.getCallType() != CallTypes.INLINE)
         {
            return false;
         }
      }
      ///or a block block
      if(block.getType() == BlockTypes.BLOCK)
      {
         BlockBlock tempBlock = (BlockBlock)block;
         if(tempBlock.getCallType() != CallTypes.INLINE)
         {
            return false;
         }
      }
      ///Make sure it isn't already in the list
      if(this.inlineBlocks.contains(block))
      {
         return false;
      }
      return true;
   }
   
   ///Adds a block to the list of inline blocks
   public void addInlineBlock(BlockInterface block)
      throws BlockException
   {
      if(!isAddableInlineBlock(block))
      {
         throw new BlockException(block.getName() + " can not be added as inline block");
      }
      ///Add it to the list
      this.inlineBlocks.add(block);
   }
   
   ///Adds a block at the specified location to the list of inline blocks
   public void addInlineBlock(BlockInterface block, int index)
      throws BlockException
   {
      if(!isAddableInlineBlock(block))
      {
         throw new BlockException(block.getName() + " can not be added as inline block");
      }
      ///Add it to the list
      this.inlineBlocks.add(index,block);   
   }
   
   ///Removes the specified block from the inline list
   public void removeInlineBlock(BlockInterface block)
   {
      this.inlineBlocks.remove(block);
   }
   
   ///Returns the list of inline blocks for this class
   public List<BlockInterface> getInlineBlocks()
   {
      return inlineBlocks;
   }
   
 ///Runs a set of checks to ensure a block can be added to the method list
   private boolean isAddableMethodBlock(BlockInterface block)
   {
    ///Make sure we aren't adding an unsupported block type
      if(block.getType() != BlockTypes.CODE
            && block.getType() != BlockTypes.BLOCK)
      {
         return false;
      }
      ///And make sure it's a method version of it's a code block
      if(block.getType() == BlockTypes.CODE)
      {
         CodeBlock tempBlock = (CodeBlock)block;
         if(tempBlock.getCallType() != CallTypes.METHOD)
         {
            return false;
         }
      }
      ///or a block block
      if(block.getType() == BlockTypes.BLOCK)
      {
         BlockBlock tempBlock = (BlockBlock)block;
         if(tempBlock.getCallType() != CallTypes.METHOD)
         {
            return false;
         }
      }
      ///Make sure it isn't already in the list
      if(this.methodBlocks.contains(block))
      {
         return false;
      }
      return true;
   }
   
   ///Adds a block to the list of method blocks
   public void addMethodBlock(BlockInterface block)
      throws BlockException
   {
      if(!isAddableMethodBlock(block))
      {
         throw new BlockException(block.getName() + " can not be added as method block");
      }
      ///Add it to the list
      this.methodBlocks.add(block);
   }
   
   ///Adds a block at the specified location to the list of method blocks
   public void addMethodBlock(BlockInterface block, int index)
      throws BlockException
   {
      if(!isAddableMethodBlock(block))
      {
         throw new BlockException(block.getName() + " can not be added as method block");
      }
      ///Add it to the list
      this.methodBlocks.add(index,block);   
   }
   
   ///Removes the specified block from the method list
   public void removeMethodBlock(BlockInterface block)
   {
      this.methodBlocks.remove(block);
   }
   
   ///Returns the list of method blocks for this class
   public List<BlockInterface> getMethodBlocks()
   {
      return methodBlocks;
   }
   
   ///scans through the list of variables to see if there's already one
   ///with the same name
   public boolean varNameExists(String name)
   {
      Iterator<VarBlock> it = vars.iterator();
      while(it.hasNext())
         if(it.next().getName().equals(name))
            return true;
      return false;
   }
   
   ///scans through the list of blocks to see if there's already one with
   ///the same name
   public boolean blockNameExists(String name)
   {
      ///check inline blocks first
      Iterator<BlockInterface> it = inlineBlocks.iterator();
      while(it.hasNext())
         if(it.next().getName().equals(name))
            return true;
      ///then check method blocks
      it = methodBlocks.iterator();
      while(it.hasNext())
         if(it.next().getName().equals(name))
            return true;
      return false;
   }
   

}
